﻿using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Specialized;

namespace Legend.Web.QueryString
{
    /// <summary>
    /// Handles the rendering and parsing of query strings
    /// to query-objects.
    /// </summary>
    public class QueryStringManager
    {
        IQueryStringSerializer serializer;

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="serializer">The serializer to use to serialize
        /// and parse query strings.</param>
        /// <exception cref="ArgumentNullException">The serializer is null.</exception>
        public QueryStringManager(IQueryStringSerializer serializer)
        {
            Require.ThatArgument(serializer).Named("serializer").IsNotNull();
            this.serializer = serializer;
        }

        /// <summary>
        /// Renders the specified query to a query string using
        /// the serializer passed in the constructor.
        /// </summary>
        /// <param name="query">The query to render.</param>
        /// <returns>The rendered query string.</returns>
        public string Render(object query)
        {
            var accessor = ResolveParameterAccessor(query);
            var keysAndValues = accessor.GetAllParametersAndValues();

            return this.serializer.Serialize(keysAndValues);
        }

        /// <summary>
        /// Applies the values found in the specified query string
        /// to the specified queryObject.
        /// </summary>
        /// <param name="queryString">The query string containing the values. May be an url
        /// with query string appended.</param>
        /// <param name="query">The object to apply the query string values to.</param>
        public void ApplyQueryStringValuesToQueryObject(string queryString, object query)
        {
            var accessor = ResolveParameterAccessor(query);
            var strippedQueryString = StripQueryString(queryString);
            var keysAndValues = this.serializer.Parse(strippedQueryString);

            foreach (var pair in keysAndValues)
            {
                accessor.ApplyParameterValue(pair.Key, pair.Value);
            }
        }

        /// <summary>
        /// Applies the values in the specified query string NameValueCollection
        /// to the specified query object..
        /// </summary>
        /// <param name="queryString">The query string to apply to the query
        /// object.</param>
        /// <param name="query">The object to apply the query string values to.</param>
        public void ApplyQueryStringValuesToQueryObject(NameValueCollection queryString, object query)
        {
            Require.ThatArgument(queryString).Named("queryString").IsNotNull();

            var accessor = ResolveParameterAccessor(query);

            foreach (var parameter in queryString.Keys.Cast<string>())
            {
                accessor.ApplyParameterValue(parameter, queryString[parameter]);
            }
        }

        /// <summary>
        /// Appends the specified query object as a query string to
        /// the specified url.
        /// </summary>
        /// <param name="url">The url to append the query to.</param>
        /// <param name="query">The query to append to the url.</param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")]
        public string Append(string url, object query)
        {
            Require.ThatArgument(url).Named("url").IsNotNullOrEmpty();
            Require.ThatArgument(query).Named("query").IsNotNull();
            
            var queryString = this.Render(query);

            if (!string.IsNullOrEmpty(queryString))
            {
                return string.Concat(url, "?", queryString);
            }
            else
            {
                return url;
            }
        }

        /// <summary>
        /// Appends the specified query to the url.
        /// </summary>
        /// <param name="url">The url to append the query to.</param>
        /// <param name="query">The query to append.</param>
        /// <returns>An url with the query appended.</returns>
        /// <exception cref="ArgumentNullException">The specified url
        /// is null - or - the specified query is null </exception>
        /// <exception cref="ArgumentException">The specified url already
        /// has a query.</exception>
        public Uri Append(Uri url, object query)
        {
            Require.ThatArgument(url).Named("url").IsNotNull();
            Require.ThatArgument(query).Named("query").IsNotNull();

            if (UrlHasQuery(url))
            {
                throw new ArgumentException("Can not append query string to an url that already contains a query.");
            }

            var uri = new UriBuilder(url);
            uri.Query = this.Render(query);

            return uri.Uri;
        }

        private static bool UrlHasQuery(Uri url)
        {
            return url.Query.Length != 0;
        }

        private static string StripQueryString(string urlOrQueryString)
        {
            int questionMarkIndex = urlOrQueryString.IndexOf('?');

            if (questionMarkIndex != -1)
            {
                return urlOrQueryString.Substring(questionMarkIndex + 1, urlOrQueryString.Length - questionMarkIndex - 1);
            }
            else
            {
                return urlOrQueryString;
            }
        }

        private static IParameterAccessor ResolveParameterAccessor(object query)
        {
            var accessor = query as IParameterAccessor;

            if (accessor == null)
            {
                accessor = new AttributedParameterAccessor(query);
            }

            return accessor;
        }
    }
}
